using System;
using System.Collections;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;

namespace Noix.IMAP
{

    public delegate void ReceiveIMAPMessageDelegate(object sender, IMAPMessageEventArgs e);

    public class IMAPClient
    {
        private Socket m_pSocket = null;
        private bool _connected = false;
        private bool _authenticated = false;
        private IMAPFolder _selectedFolder = null;

        public static int RESULT_OK = 0x0;
        public static int RESULT_NO = 0x1;
        public static int RESULT_BAD = 0x2;
        public static int RESULT_UNKNOWN = 0x3;

        public static int RESPONSE_MAX_LEN = 1000;
        public static int RESPONSE_TIMEOUT = 60000;

        public event ReceiveIMAPMessageDelegate ReceiveMessage;

        private void OnReceiveMessage(IMAPMessage message)
        {
            // Safely invoke an event:
            ReceiveIMAPMessageDelegate temp = ReceiveMessage;

            if (temp != null)
            {
                temp(this, new IMAPMessageEventArgs(message));
            }
        }



        /// <summary>
        /// Sends line to IMAP server.
        /// </summary>
        /// <param name="lineData"> Data. </param>
        private void SendLine(string lineData)
        {
            Console.WriteLine("Send    : <" + lineData + ">");
            byte[] msg = Encoding.ASCII.GetBytes(lineData + "\r\n");
            int bytesSent = m_pSocket.Send(msg);
        }

        /// <summary>
        /// Reads line from IMAP server.
        /// </summary>    
        /// <returns> Readed string. </returns> 
        private string ReadLine()
        {
            long lastDataTime = DateTime.Now.Ticks;
            ArrayList lineBuf = new ArrayList();
            byte prevByte = 0;

            while (true)
            {
                if (m_pSocket.Available > 0)
                {
                    byte[] currByte = new byte[1];
                    int countRecieved = m_pSocket.Receive(currByte, 1, SocketFlags.None);
                    // Count must be equal. Eg. some computers won't give byte at first read attempt
                    if (countRecieved == 1)
                    {
                        lineBuf.Add(currByte[0]);

                        // Line found
                        if ((prevByte == (byte)'\r' && currByte[0] == (byte)'\n'))
                        {
                            byte[] retVal = new byte[lineBuf.Count - 2];    // Remove <CRLF> 
                            lineBuf.CopyTo(0, retVal, 0, lineBuf.Count - 2);

                            Console.WriteLine("Receive : <" + System.Text.Encoding.ASCII.GetString(retVal) + ">");
                            return System.Text.Encoding.ASCII.GetString(retVal).Trim();
                        }

                        prevByte = currByte[0];

                        if (lineBuf.Count > RESPONSE_MAX_LEN)
                        {
                            throw new Exception("Maximum line length exceeded");
                        }

                        // reset last data time
                        lastDataTime = DateTime.Now.Ticks;
                    }
                }
                else
                {
                    if (DateTime.Now.Ticks > lastDataTime + ((long)(RESPONSE_TIMEOUT)) * 10000)
                    {
                        throw new Exception("Read timeout");
                    }
                    System.Threading.Thread.Sleep(100);
                }
            }
        }

        /// <summary>
        /// Reads message from IMAP server.
        /// </summary>
        /// <param name="messageLength"> Length of the message. </param>
        /// <returns> Readed string. </returns> 
        private string ReadMessage(int messageLength)
        {
            long lastDataTime = DateTime.Now.Ticks;
            ArrayList lineBuf = new ArrayList();
            int length = 0;

            while (true)
            {
                if (m_pSocket.Available > 0)
                {
                    byte[] currByte = new byte[1];
                    int countRecieved = m_pSocket.Receive(currByte, 1, SocketFlags.None);
                    // Count must be equal. Eg. some computers won't give byte at first read attempt
                    if (countRecieved == 1)
                    {
                        lineBuf.Add(currByte[0]);
                        length++;

                        // Line found
                        if (length >= messageLength)
                        {
                            byte[] retVal = new byte[lineBuf.Count];
                            lineBuf.CopyTo(0, retVal, 0, lineBuf.Count);

                            //Console.WriteLine("Receive msg: <" + System.Text.Encoding.ASCII.GetString(retVal)+ ">");
                            //Content-Transfer-Encoding: 8bit
                            return System.Text.Encoding.ASCII.GetString(retVal).Trim();
                        }

                        // reset last data time
                        lastDataTime = DateTime.Now.Ticks;
                    }
                }
                else
                {
                    if (DateTime.Now.Ticks > lastDataTime + ((long)(RESPONSE_TIMEOUT)) * 10000)
                    {
                        throw new Exception("Read timeout");
                    }
                    System.Threading.Thread.Sleep(100);
                }
            }
        }

        /// <summary>
        /// Gets reply from readed line.
        /// </summary>
        /// <param name="lineData"> Data. </param>
        /// <returns> Reply. </returns>
        private int GetReply(string reply)
        {

            reply = reply.Substring(reply.IndexOf(" ")).Trim(); // Remove Cmd tag
            if (reply.ToUpper().StartsWith("OK")) return RESULT_OK;
            if (reply.ToUpper().StartsWith("NO")) return RESULT_NO;
            if (reply.ToUpper().StartsWith("BAD")) return RESULT_BAD;
            return RESULT_UNKNOWN;
        }

        private string[] GetReplyLignes(ref string reply)
        {

            ArrayList list = new ArrayList();

            reply = ReadLine();
            if (reply.StartsWith("*"))
            {
                while (reply.StartsWith("*"))
                {
                    string folder = reply.Substring(reply.IndexOf(" ")).Trim();
                    folder = folder.Substring(folder.IndexOf(" ")).Trim();
                    folder = SkipBrackets(folder).Trim();
                    folder = folder.Substring(folder.IndexOf(" ")).Trim().Replace("\"", "");
                    /*string folder = line.Substring(line.LastIndexOf(" ")).Trim().Replace("\"","");*/
                    //Console.WriteLine("Trace : <" + folder + ">");
                    list.Add(folder);
                    reply = ReadLine();
                }
            }
            string[] retVal = new string[list.Count];
            list.CopyTo(retVal);

            return retVal;

        }

        /// <summary>
        /// Remove string in brackets.
        /// </summary>    
        /// <param name="s"> String for modifing. </param>
        /// <returns> Modified string. </returns>
        private string SkipBrackets(string s)
        {
            int i = s.IndexOf("(");
            int j = s.IndexOf(")");
            if (j > i)
            {
                if (i > 1) return s.Substring(0, i) + s.Substring(j + 1);
                else return s.Substring(j + 1);
            }
            else return null;
        }

        private string GetString(string str, string begin, string end)
        {
            string result = null;
            if (str.IndexOf(begin) > -1)
            {
                str = str.Substring(str.IndexOf(begin) + begin.Length);
                if (str.IndexOf(end) > -1) result = str.Substring(0, str.IndexOf(end));
                else result = str;
            }
            return result;
        }

        private string GetMessageUID(string reply)
        {
            return GetString(reply, "UID ", " ");
        }

        private string GetMessageFrom(string reply)
        {
            return GetString(reply, "From: ", "\r\n");
        }

        private string GetMessageSubject(string reply)
        {
            return GetString(reply, "Subject: ", "\r\n");
        }

        private DateTime GetMessageDate(string reply)
        {
            string result = GetString(reply, "Date: ", "\r\n");
            if ((result == null) || (result.Length == 0)) return DateTime.Now;
            if (result.IndexOf(":") > -1)
            {
                result = result.Substring(0, result.IndexOf(":") + 6);
            }
            return Convert.ToDateTime(result.Trim());
        }

        private string GetMessageTo(string reply)
        {
            return GetString(reply, "To: ", "\r\n");
        }

        private string GetMessageFlags(string reply)
        {
            return GetString(reply, "FLAGS (", ")");
        }

        /// <summary>
        /// Connects to IMAP server.
        /// </summary>
        /// <param name="host"> Host name. </param>
        /// <param name="port"> Port. </param>
        public void Connect(string host, int port)
        {
            if (!_connected)
            {
                m_pSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                IPEndPoint ipdest = new IPEndPoint(Dns.GetHostEntry(host).AddressList[0], port);
                m_pSocket.Connect(ipdest);

                string reply = ReadLine();

                if (GetReply(reply) != RESULT_OK)
                {
                    m_pSocket.Close();
                    m_pSocket = null;
                    throw new Exception("Server returned:" + reply);
                }

                _connected = true;
            }
        }

        /// <summary>
        /// Disconnects from IMAP server.
        /// </summary>
        public void Disconnect()
        {
            if (m_pSocket != null && m_pSocket.Connected)
            {
                SendLine("a1 LOGOUT");

                m_pSocket.Close();
                m_pSocket = null;
            }

            _connected = false;
            _authenticated = false;
        }

        public void Authenticate(string userName, string password)
        {
            if (!_connected)
            {
                throw new Exception("You must connect first !");
            }
            if (_authenticated)
            {
                throw new Exception("You are already authenticated !");
            }

            SendLine("a1 LOGIN " + userName + " " + password);

            string reply = ReadLine();

            if (GetReply(reply) != RESULT_OK)
            {
                throw new Exception("Server returned:" + reply);
            }
            _authenticated = true;
        }

        /// <summary>
        /// Creates specified folder to IMAP server.
        /// </summary>
        /// <param name="folderName">Folder name. Eg. test, Inbox\SomeSubFolder.</param>
        public void CreateFolder(string folderName)
        {
            if (!_connected)
            {
                throw new Exception("You must connect first !");
            }
            if (!_authenticated)
            {
                throw new Exception("You must authenticate first !");
            }

            SendLine("a1 CREATE \"" + folderName + "\"");
            string reply = ReadLine();

            if (GetReply(reply) != RESULT_OK)
            {
                throw new Exception("Server returned:" + reply);
            }

        }

        /// <summary>
        /// Deletes specified folder to IMAP server.
        /// </summary>
        /// <param name="folderName">Folder name.</param>
        public void DeleteFolder(string folderName)
        {
            if (!_connected)
            {
                throw new Exception("You must connect first !");
            }
            if (!_authenticated)
            {
                throw new Exception("You must authenticate first !");
            }

            SendLine("a1 DELETE \"" + folderName + "\"");
            string reply = ReadLine();

            if (GetReply(reply) != RESULT_OK)
            {
                throw new Exception("Server returned:" + reply);
            }
        }

        /// <summary>
        /// Renames specified folder to IMAP server.
        /// </summary>
        /// <param name="sourceFolderName">Source folder name.</param>
        /// <param name="destinationFolderName">Destination folder name.</param>
        public void RenameFolder(string sourceFolderName, string destinationFolderName)
        {
            if (!_connected)
            {
                throw new Exception("You must connect first !");
            }
            if (!_authenticated)
            {
                throw new Exception("You must authenticate first !");
            }

            SendLine("a1 RENAME \"" + sourceFolderName + "\" \"" + destinationFolderName + "\"");
            string reply = ReadLine();

            if (GetReply(reply) != RESULT_OK)
            {
                throw new Exception("Server returned:" + reply);
            }
        }

        public string[] GetSubscribedFolders(string fldr)
        {
            if (!_connected)
            {
                throw new Exception("You must connect first !");
            }
            if (!_authenticated)
            {
                throw new Exception("You must authenticate first !");
            }

            SendLine("a1 LSUB \"\" \"" + fldr + "\"");

            string reply = "";
            string[] retVal = GetReplyLignes(ref reply);
            if (GetReply(reply) != RESULT_OK)
            {
                throw new Exception("Server returned:" + reply);
            }

            return retVal;
        }

        public string[] GetFolders(string fldr)
        {
            if (!_connected)
            {
                throw new Exception("You must connect first !");
            }
            if (!_authenticated)
            {
                throw new Exception("You must authenticate first !");
            }

            SendLine("a1 LIST \"\" \"" + fldr + "\"");

            string reply = "";
            string[] retVal = GetReplyLignes(ref reply);
            if (GetReply(reply) != RESULT_OK)
            {
                throw new Exception("Server returned:" + reply);
            }

            return retVal;
        }

        /// <summary>
        /// Subscribes specified folder.
        /// </summary>
        /// <param name="folderName">Folder name.</param>
        public void SubscribeFolder(string folderName)
        {
            if (!_connected)
            {
                throw new Exception("You must connect first !");
            }
            if (!_authenticated)
            {
                throw new Exception("You must authenticate first !");
            }

            SendLine("a1 SUBSCRIBE \"" + folderName + "\"");

            string reply = ReadLine();

            if (GetReply(reply) != RESULT_OK)
            {
                throw new Exception("Server returned:" + reply);
            }
        }

        /// <summary>
        /// UnSubscribes specified folder.
        /// </summary>
        /// <param name="folderName">Folder name,</param>
        public void UnSubscribeFolder(string folderName)
        {
            if (!_connected)
            {
                throw new Exception("You must connect first !");
            }
            if (!_authenticated)
            {
                throw new Exception("You must authenticate first !");
            }

            SendLine("a1 UNSUBSCRIBE \"" + folderName + "\"");
            string reply = ReadLine();

            if (GetReply(reply) != RESULT_OK)
            {
                throw new Exception("Server returned:" + reply);
            }
        }

        public void SelectFolder(string folderName)
        {
            int exists = -1;
            int recent = -1;

            if (!_connected)
            {
                throw new Exception("You must connect first !");
            }
            if (!_authenticated)
            {
                throw new Exception("You must authenticate first !");
            }

            SendLine("a1 SELECT \"" + folderName + "\"");

            string reply = ReadLine();
            if (reply.StartsWith("*"))
            {
                while (reply.StartsWith("*"))
                {
                    if (reply.IndexOf(" EXISTS") > -1) exists = Convert.ToInt32(reply.Substring(2, reply.IndexOf(" EXISTS") - 2));
                    if (reply.IndexOf(" RECENT") > -1) recent = Convert.ToInt32(reply.Substring(2, reply.IndexOf(" RECENT") - 2));
                    reply = ReadLine();
                }
            }

            if (GetReply(reply) != RESULT_OK)
            {
                throw new Exception("Server returned:" + reply);
            }

            _selectedFolder = new IMAPFolder(folderName);
            _selectedFolder.Exists = exists;
            _selectedFolder.Recent = recent;
        }

        /// <summary>
        /// Fetch messages.
        /// </summary>
        /// <param name="startMsgNo">Start message number.</param>
        /// <param name="endMsgNo">End message number. -1 = last.</param>
        /// <returns> Array of IMAPMessage. </returns>
        public IMAPMessage[] Fetch(int startMsgNo, int endMsgNo, bool withText)
        {
            if (!_connected)
            {
                throw new Exception("You must connect first !");
            }
            if (!_authenticated)
            {
                throw new Exception("You must authenticate first !");
            }
            if (_selectedFolder == null)
            {
                throw new Exception("You must select folder first !");
            }

            string endMsg = endMsgNo.ToString();
            if (endMsgNo < 1)
            {
                endMsg = "*";
            }

            ArrayList messageBuf = new ArrayList();

            /*SendLine("a1 FETCH 1:* (UID RFC822.SIZE FLAGS BODY[HEADER.FIELDS (DATE FROM SUBJECT TO)])");*/

            string command;
            if (withText) 
                command = "a1 FETCH " + startMsgNo + ":" + endMsg + " (UID RFC822.SIZE FLAGS BODY.PEEK[HEADER] BODY.PEEK[TEXT])";
            else
                command = "a1 FETCH " + startMsgNo + ":" + endMsg + " (UID RFC822.SIZE FLAGS BODY.PEEK[HEADER])";
            SendLine(command);
            string reply = ReadLine();
            if (reply.StartsWith("*"))
            {
                while (reply.StartsWith("*"))
                {
                    IMAPMessage message = null;
                    string msg = ReadMessage(Convert.ToInt32(reply.Substring(reply.LastIndexOf("{") + 1, reply.LastIndexOf("}") - reply.LastIndexOf("{") - 1)));
                    string uid = GetMessageUID(reply);
                    if (uid.Length > 0)
                    {
                        message = new IMAPMessage(uid);
                        message.From = GetMessageFrom(msg);
                        message.To = GetMessageTo(msg);
                        message.Subject = CanonicalDecode(GetMessageSubject(msg));
                        message.Date = GetMessageDate(msg);
                        message.SetFlags(GetMessageFlags(reply));

                        if (withText)
                        {
                            reply = ReadLine(); // read BODY.PEEK[TEXT]
                            message.Text = ReadMessage(Convert.ToInt32(reply.Substring(reply.LastIndexOf("{") + 1, reply.LastIndexOf("}") - reply.LastIndexOf("{") - 1)));
                        }
                        OnReceiveMessage(message);

                        messageBuf.Add(message);
                    }
                    reply = ReadLine(); // read ")"
                    reply = ReadLine();
                }
            }

            if (GetReply(reply) != RESULT_OK)
            {
                throw new Exception("Server returned:" + reply);
            }

            IMAPMessage[] retVal = new IMAPMessage[messageBuf.Count];
            messageBuf.CopyTo(retVal);

            return retVal;
        }

        /// <summary>
        /// Expunge.
        /// </summary>
        public void Expunge()
        {
            if (!_connected)
            {
                throw new Exception("You must connect first !");
            }
            if (!_authenticated)
            {
                throw new Exception("You must authenticate first !");
            }
            if (_selectedFolder == null)
            {
                throw new Exception("You must select folder first !");
            }

            SendLine("a1 EXPUNGE");

            string reply = ReadLine();
            if (reply.StartsWith("*"))
            {
                while (reply.StartsWith("*"))
                {
                    reply = ReadLine();
                }
            }

            if (GetReply(reply) != RESULT_OK)
            {
                throw new Exception("Server returned:" + reply);
            }
        }

        /// <summary>
        /// Makes copy of messages to specified folder.
        /// </summary>
        /// <param name="startMsgNo">Start message number.</param>
        /// <param name="endMsgNo">End message number. -1 = last.</param>
        /// <param name="destFolder">Folder where to cpoy messages.</param>
        public void CopyMessages(int startMsgNo, int endMsgNo, string destFolder)
        {
            if (!_connected)
            {
                throw new Exception("You must connect first !");
            }
            if (!_authenticated)
            {
                throw new Exception("You must authenticate first !");
            }
            if (_selectedFolder == null)
            {
                throw new Exception("You must select folder first !");
            }

            string endMsg = endMsgNo.ToString();
            if (endMsgNo < 1)
            {
                endMsg = "*";
            }

            SendLine("a1 UID COPY " + startMsgNo + ":" + endMsg + " \"" + destFolder + "\"");

            string reply = ReadLine();

            if (GetReply(reply) != RESULT_OK)
            {
                throw new Exception("Server returned:" + reply);
            }
        }

        /// <summary>
        /// Set flags to specified message.
        /// </summary>
        /// <param name="msg">Message.</param>
        public void SetFlags(IMAPMessage msg)
        {
            if (!_connected)
            {
                throw new Exception("You must connect first !");
            }
            if (!_authenticated)
            {
                throw new Exception("You must authenticate first !");
            }
            if (_selectedFolder == null)
            {
                throw new Exception("You must select folder first !");
            }

            SendLine("a1 UID STORE " + msg.Uid + ":" + msg.Uid + " FLAGS.SILENT (" + msg.GetFlags() + ")");

            string reply = ReadLine();

            if (GetReply(reply) != RESULT_OK)
            {
                throw new Exception("Server returned:" + reply);
            }
        }

        /// <summary>
        /// Canonical decoding. Decodes all canonical encoding occurences in specified text.
        /// Usually mime message header unicode/8bit values are encoded as Canonical.
        /// </summary>
        /// <param name="text">Text to decode.</param>
        /// <returns>Returns decoded text.</returns>
        public static string CanonicalDecode(string text)
        {
            // =?charSet?type[Q or B]?encoded string?=

            if (text == null) return text;
            Regex regex = new Regex(@"\=\?(?<charSet>[\w\-]*)\?(?<type>[qQbB])\?(?<text>[\w\W]*)\?\=");

            MatchCollection m = regex.Matches(text);
            foreach (Match match in m)
            {
                try
                {
                    System.Text.Encoding enc = System.Text.Encoding.GetEncoding(match.Groups["charSet"].Value);
                    // QDecode
                    if (match.Groups["type"].Value.ToLower() == "q")
                    {
                        text = text.Replace(match.Value, QDecode(enc, match.Groups["text"].Value));
                    }
                    // Base64
                    else
                    {
                        text = text.Replace(match.Value, enc.GetString(Convert.FromBase64String(match.Groups["text"].Value)));
                    }
                }
                catch
                {
                    // If parsing fails, just leave this string as is
                }
            }
            return text;
        }

        /// <summary>
        /// "Q" decoder. This is same as quoted-printable, except '_' is converted to ' '.
        /// </summary>
        /// <param name="encoding">Input string encoding.</param>
        /// <param name="data">String which to encode.</param>
        /// <returns>Returns decoded string.</returns>    
        public static string QDecode(System.Text.Encoding encoding, string data)
        {
            return QuotedPrintableDecode(encoding, System.Text.Encoding.ASCII.GetBytes(data)).Replace("_", " ");
        }

        /// <summary>
        /// quoted-printable decoder.
        /// </summary>
        /// <param name="encoding">Input string encoding.</param>
        /// <param name="data">Data which to encode.</param>
        /// <returns>Returns decoded data with specified encoding.</returns>
        public static string QuotedPrintableDecode(System.Text.Encoding encoding, byte[] data)
        {
            MemoryStream strm = new MemoryStream(data);
            int b = strm.ReadByte();

            MemoryStream dStrm = new MemoryStream();

            while (b > -1)
            {
                // Hex eg. =E4
                if (b == '=')
                {
                    byte[] buf = new byte[2];
                    strm.Read(buf, 0, 2);

                    // <CRLF> followed by =, it's splitted line
                    if (!(buf[0] == '\r' && buf[1] == '\n'))
                    {
                        try
                        {
                            int val = int.Parse(System.Text.Encoding.ASCII.GetString(buf), System.Globalization.NumberStyles.HexNumber);
                            string encodedChar = encoding.GetString(new byte[] { (byte)val });
                            byte[] d = System.Text.Encoding.Unicode.GetBytes(encodedChar);
                            dStrm.Write(d, 0, d.Length);
                        }
                        catch
                        { // If worng hex value, just skip this chars
                        }
                    }
                }
                else
                {
                    string encodedChar = encoding.GetString(new byte[] { (byte)b });
                    byte[] d = System.Text.Encoding.Unicode.GetBytes(encodedChar);
                    dStrm.Write(d, 0, d.Length);
                }

                b = strm.ReadByte();
            }

            return System.Text.Encoding.Unicode.GetString(dStrm.ToArray());
        }
    }
}
